Skip to content

feat: scope CLI state files by API key to isolate concurrent accounts#44

Open
andreakiro wants to merge 1 commit into
fix/parallel-sessions-start-racefrom
feat/scope-current-session-by-api-key
Open

feat: scope CLI state files by API key to isolate concurrent accounts#44
andreakiro wants to merge 1 commit into
fix/parallel-sessions-start-racefrom
feat/scope-current-session-by-api-key

Conversation

@andreakiro
Copy link
Copy Markdown
Member

Summary

Stacked on top of #43 (review and merge #43 first; this PR's base is fix/parallel-sessions-start-race).

Adds the B1 hardening from the original PRD: per-API-key scoping for the four CLI state files (current_session, current_agent, current_session_expiry, current_viewer_url). Two accounts running the CLI on the same machine no longer share a single ~/.notte/cli/current_session slot, so a sessions stop from shell A can't accidentally target the session belonging to shell B.

#43 already closed the parallel-worker race in non-TTY contexts (the urgent bug). This PR closes the related multi-account collision in interactive use, and makes a per-account workflow (e.g. notte auth login + notte sessions start in two separate terminals) actually isolated.

How it works

  • Compute a suffix .<sha256(api_key)[:8]> once per process. When no API key is configured (e.g. mid notte auth login) the suffix is empty and behavior matches the legacy file paths.
  • Write: scoped path only - ~/.notte/cli/current_session.abc12345.
  • Read: scoped path first, fall back to the legacy unscoped path so users upgrading from an older CLI version don't lose their existing current_session.
  • Clear: removes both the scoped and the legacy unscoped path so upgraders don't see stale state.

sessions.go and agents.go now delegate all four state files to three helpers in internal/cmd/state_scope.go: readStateFile, writeStateFile, clearStateFile. Net effect is ~70 fewer lines in the two run files.

Backwards compatibility

  • Users with an existing ~/.notte/cli/current_session keep working: the read fallback finds it, and the next write creates the scoped copy. No migration is required and no shell session is interrupted.
  • Removing the legacy file on next clear is intentional - keeping it around invites the very confusion this PR is trying to remove.
  • No CLI command, flag, or env-var surface changes.

Test plan

  • go test ./... and go vet ./... clean
  • New unit tests in internal/cmd/state_scope_test.go:
    • TestAPIKeyScopeSuffix_FromEnvVar - suffix is derived from NOTTE_API_KEY and stable across calls
    • TestStateFilePath_WithAPIKeyScope - writes land at the suffixed path and not the legacy one
    • TestReadStateFile_FallsBackToLegacyUnscopedPath - upgrade path
    • TestReadStateFile_PrefersScopedOverLegacy - precedence when both files exist
    • TestClearStateFile_RemovesBothScopedAndLegacy - no stale state after clear
    • TestDifferentAPIKeysIsolated - two API keys read different sessions on the same machine
  • TestMain in the cmd test package pins the override to "" so the existing fleet of sessions_test.go / agents_test.go continues to operate on the legacy unscoped path
  • Manual: NOTTE_API_KEY=key-A notte sessions start in shell A and NOTTE_API_KEY=key-B notte sessions start in shell B, then sessions status in each - each shell sees its own session

Risk / rollback

Low. The change is localized to internal/cmd/state_scope.go + the four helper functions in sessions.go/agents.go. The legacy-read fallback covers the upgrade path; a downgrade after this lands would lose the scoped file (the older CLI would only read current_session), but no session is destroyed - users just need to pass --session-id until they sessions start again.

Two different accounts running the CLI on the same machine used to
share the same ~/.notte/cli/current_session (and current_agent,
current_session_expiry, current_viewer_url) file. Switching API keys
or running two shells under different accounts could cause one
account's interactive `sessions stop` to target the other account's
session. Stack on top of the non-TTY skip-write fix (#43) - that fix
already removed the parallel-worker race; this PR closes the related
multi-account collision.

- Suffix the four state files with `.<sha256(api_key)[:8]>` whenever
  an API key is configured. When no key is available (e.g. during
  `notte auth login` itself) the suffix is empty and behavior is
  unchanged.
- Read uses the scoped path first and falls back to the legacy
  unscoped path so users upgrading from an earlier CLI don't lose
  their existing current_session.
- Clear removes both the scoped and the legacy unscoped file so
  upgraders don't see stale state.
- Centralize the four "current_*" file helpers in state_scope.go
  (readStateFile / writeStateFile / clearStateFile). sessions.go and
  agents.go shrink as a result.
- New tests cover env-var derivation, scoped-write, legacy-read
  fallback, scoped-over-legacy precedence, clear-removes-both, and
  isolation between two distinct API keys.

The cmd test package pins the override to "" via TestMain so existing
tests that hand-roll state paths continue to work unchanged.
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented May 22, 2026

Greptile Summary

This PR scopes the four CLI state files (current_session, current_agent, current_session_expiry, current_viewer_url) by a short SHA-256 hash of the active API key, so two accounts on the same machine no longer share a single slot and a sessions stop from one shell cannot target another shell's session.

  • A new state_scope.go module provides readStateFile/writeStateFile/clearStateFile helpers; reads try the scoped path first and fall back to the legacy unscoped path for backwards-compatible upgrades, while clears remove both to avoid stale state.
  • sessions.go and agents.go are refactored to delegate all state-file I/O to those helpers, removing ~70 lines of duplicated path logic.
  • A dedicated test file covers suffix derivation, scoped write isolation, legacy fallback, precedence when both files exist, clear behaviour, and two-account isolation; TestMain pins the override to "" so the existing test fleet continues to operate on unscoped paths.

Confidence Score: 5/5

The change is well-contained and safe to merge; all four state files are touched consistently and the legacy read-fallback covers the upgrade path cleanly.

The implementation is straightforward: a thin scoping layer over existing file I/O, backed by thorough unit tests. There are no correctness issues in the changed paths; clearCurrentAgentIfMatches now also cleans up the legacy file on a matching ID, which is the documented intent. The only finding is an unused helper function (stateFilePath) that has no callers.

No files require special attention.

Important Files Changed

Filename Overview
internal/cmd/state_scope.go New file implementing per-API-key state file scoping with read/write/clear helpers; stateFilePath is defined but never called (dead code)
internal/cmd/state_scope_test.go Comprehensive unit tests covering suffix derivation, scoped writes, legacy fallback, precedence, clear behaviour, and account isolation; TestMain correctly pins override to "" for the full package
internal/cmd/sessions.go Refactored to delegate all four state-file operations to the new helpers; logic unchanged, net ~70 lines removed
internal/cmd/agents.go Refactored agent-ID persistence to use shared helpers; clearCurrentAgentIfMatches now removes the legacy file alongside the scoped one when the IDs match, which is the documented intent

Fix All in Claude Code

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
internal/cmd/state_scope.go:55-63
**`stateFilePath` is dead code**

`stateFilePath` is defined but never called: `readStateFile`, `writeStateFile`, and `clearStateFile` all compute the scoped path inline. This unexported helper has no callers and will generate a `go vet`/lint warning in strict configurations.

Reviews (2): Last reviewed commit: "feat: scope CLI state files by API key t..." | Re-trigger Greptile

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant